Conversation
…umerators Each call to AsyncSeq.ofSeq, or any async CE block using try...with, try...finally, or use expressions, previously heap-allocated a Ref<T> wrapper object to hold the enumerator's state machine field. Converting from: let state = ref (SomeState.NotStarted inp) ... state.Value <- SomeState.Next ... to: let mutable state = SomeState.NotStarted inp ... state <- SomeState.Next ... eliminates the Ref<T> heap allocation because the mutable local is promoted to a direct field in the compiler-generated object-expression class (the same pattern already used by collectSeq and takeWhileInclusive since 4.11.0/4.12.0). Also eliminates two short-lived Ref<Choice<_,_>> locals per tryWith invocation (used to bridge synchronous try...with inside the async block). 422/422 tests pass. Co-authored-by: Copilot <[email protected]>
dsyme
approved these changes
Apr 21, 2026
This was referenced Apr 23, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🤖 This is an automated pull request from Repo Assist.
Summary
Eliminates unnecessary heap allocations in three fundamental async sequence enumerators by replacing
refcell state fields withmutablelocals.Problem
Three enumerator implementations —
ofSeq,tryWith, andtryFinally— each allocated aRef<T>wrapper object on every call to store the enumerator's state machine variable:tryWithalso allocated two short-livedRef<Choice<_,_>>objects per invocation to bridge synchronoustry...withlogic inside theasync { }block.Fix
Replace
refwithmutable, consistent with the pattern already used bycollectSeqandtakeWhileInclusive(introduced in 4.11.0 / 4.12.0):In F# object expressions,
let mutablevalues captured by member methods are promoted to fields of the compiler-generated class, so no extra allocation is needed. TheRef<T>wrapper is entirely eliminated.Impact
ofSeq: Called any time aseq<'T>is wrapped as an async sequence. OneRef<MapState>allocation eliminated per enumeration.tryWith: Used by theasyncSeq { try ... with ... }CE builder. OneRef<TryWithState>+ twoRef<Choice>allocations eliminated per enumerator.tryFinally: Used byasyncSeq { try ... finally ... }andusebindings. OneRef<TryFinallyState>allocation eliminated per enumerator.Trade-offs
No behaviour change; purely a GC pressure reduction. The F# compiler generates equivalent IL — the difference is that the state now lives directly as a field of the enumerator class instead of as a field pointing to a separately allocated
Ref<T>object.Test Status
✅ Build: succeeded (0 errors, pre-existing warnings only)
✅ Tests: 422/422 passed (418 existing + 4 new tests for
ofSeq,tryFinallyresource disposal, andtryWithhandler yield)